package com.ihome.framework.core.nos;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.annotation.Resource;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ihome.framework.core.commons.BaseConst;
import com.ihome.framework.core.log.Log;
import com.netease.cloud.ClientException;
import com.netease.cloud.ServiceException;
import com.netease.cloud.auth.Credentials;
import com.netease.cloud.services.nos.NosClient;
import com.netease.cloud.services.nos.model.GetObjectRequest;
import com.netease.cloud.services.nos.model.NOSObject;
import com.netease.cloud.services.nos.model.NOSObjectInputStream;
import com.netease.cloud.services.nos.model.ObjectMetadata;
import com.netease.cloud.services.nos.transfer.TransferManager;
import com.netease.cloud.services.nos.transfer.Upload;
import com.netease.cloud.services.nos.transfer.model.UploadResult;

/**
 * 包装了一下nosClient对象
 * 
 * @author zhengxiaohong
 *
 */
public class IHomeNosClient extends NosClient {
	private static final Logger LOG = LoggerFactory.getLogger(IHomeNosClient.class);

	@Resource
	private NosConfig nosConfig;

	@Resource
	private TransferManager transferManager;

	@Resource
	private IHomeNosOldClient iHomeNosOldClient;

	private static final int THREAD_COUNT = 3;
	private static final int ZIP_BUFFER_SIZE = 1024;
	private static final int DOWNLOAD_BLOCK_SIZE = 50 * 1024 * 1024;

	/**
	 * 新建IHomeNosClient实例
	 */
	public IHomeNosClient() {
		super();
	}

	/**
	 * 新建IHomeNosClient实例
	 * 
	 * @param Credentials
	 *            秘钥相关
	 * @param hostName
	 *            nos的服务器名
	 */
	public IHomeNosClient(Credentials Credentials, String hostName) {
		super(Credentials);
		setEndpoint(hostName);
	}

	/**
	 * 获取对象元数据
	 * 
	 * @param getObjectRequest
	 *            请求对象
	 * @param destinationFile
	 *            目标文件
	 * @return 对象元数据
	 * @throws ClientException
	 *             客户端异常
	 * @throws ServiceException
	 *             服务端异常
	 */
	public ObjectMetadata getObject(GetObjectRequest getObjectRequest, File destinationFile)
			throws ClientException, ServiceException {
		LOG.info(Log.op("getObject with file").msg("get object with new sdk start").toString());
		if (getObjectRequest == null || destinationFile == null) {
			return null;
		}
		String bucketName = getObjectRequest.getBucketName();
		String key = getObjectRequest.getKey();
		boolean isOpen = Boolean.parseBoolean(nosConfig.getIsOpen());
		if (isOpen) {
			LOG.info(Log.op("getObject with file").msg("get object with new sdk").kv("isOpen", isOpen).kv("key", key)
					.toString());
			boolean flag = super.doesObjectExist(bucketName, key);
			if (flag) {
				LOG.info(Log.op("getObject with file").msg("get object with new sdk from newbucket")
						.kv("isOpen", isOpen).kv("key", key).toString());
				return super.getObject(getObjectRequest, destinationFile);
			} else {
				LOG.info(Log.op("getObject with file").msg("get object with new sdk from oldbucket")
						.kv("isOpen", isOpen).kv("key", key).toString());
				return iHomeNosOldClient.getObject(getObjectRequest, destinationFile);
			}
		} else {
			LOG.info(Log.op("getObject with file").msg("get object with new sdk without old bucket")
					.kv("isOpen", isOpen).kv("key", key).toString());
			return super.getObject(getObjectRequest, destinationFile);
		}
	}

	/**
	 * 获取对象元数据
	 * 
	 * @param getObjectRequest
	 *            请求对象
	 * @return 对象元数据
	 * @throws ClientException
	 *             客户端异常
	 * @throws ServiceException
	 *             服务端异常
	 */
	public NOSObject getObject(GetObjectRequest getObjectRequest) throws ClientException, ServiceException {
		LOG.info(Log.op("getObject").msg("get object with new sdk start").toString());
		if (getObjectRequest == null) {
			return null;
		}
		String bucketName = getObjectRequest.getBucketName();
		String key = getObjectRequest.getKey();
		boolean isOpen = Boolean.parseBoolean(nosConfig.getIsOpen());
		if (isOpen) {
			LOG.info(Log.op("getObject").msg("get object with new sdk").kv("isOpen", isOpen).kv("key", key).toString());
			boolean flag = super.doesObjectExist(bucketName, key);
			if (flag) {
				LOG.info(Log.op("getObject").msg("get object with new sdk from newbucket").kv("isOpen", isOpen)
						.kv("key", key).toString());
				return super.getObject(getObjectRequest);
			} else {
				LOG.info(Log.op("getObject").msg("get object with new sdk from oldbucket").kv("isOpen", isOpen)
						.kv("key", key).toString());
				return iHomeNosOldClient.getObject(getObjectRequest);
			}
		} else {
			LOG.info(Log.op("getObject").msg("get object with new sdk without old bucket").kv("isOpen", isOpen)
					.kv("key", key).toString());
			return super.getObject(getObjectRequest);
		}
	}

	/**
	 * 判断对象是否存在
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @return 文件对象是否存在
	 */
	public boolean doesObjectExist(String bucketName, String key) {
		LOG.info(Log.op("doesObjectExist").msg("doesObjectExist with new sdk start").toString());
		boolean isOpen = Boolean.parseBoolean(nosConfig.getIsOpen());
		if (isOpen) {
			LOG.info(Log.op("doesObjectExist").msg("doesObjectExist with new sdk").kv("isOpen", isOpen).kv("key", key)
					.toString());
			boolean flag = super.doesObjectExist(bucketName, key);
			if (!flag) {
				LOG.info(Log.op("doesObjectExist").msg("doesObjectExist with new sdk from oldbucket")
						.kv("isOpen", isOpen).kv("key", key).toString());
				flag = iHomeNosOldClient.doesObjectExist(bucketName, key);
			}
			return flag;
		} else {
			LOG.info(Log.op("doesObjectExist").msg("doesObjectExist with new sdk without oldbucket")
					.kv("isOpen", isOpen).kv("key", key).toString());
			return super.doesObjectExist(bucketName, key);
		}
	}

	/**
	 * 判断对象是否存在于新环境的nos
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @return 文件对象是否存在
	 */
	public boolean hasObject(String bucketName, String key) {
		return super.doesObjectExist(bucketName, key);
	};

	/**
	 * 删除对象
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 */
	public void deleteObject(String bucketName, String key) throws ClientException, ServiceException {
		LOG.info(Log.op("deleteObject").msg("deleteObject with new sdk start").toString());
		boolean isOpen = Boolean.parseBoolean(nosConfig.getIsOpen());
		if (isOpen) {
			boolean flag = super.doesObjectExist(bucketName, key);
			if (flag) {
				LOG.info(Log.op("deleteObject").msg("deleteObject with new sdk only in newbucket").kv("isOpen", isOpen)
						.kv("key", key).toString());
				super.deleteObject(bucketName, key);
				return;
			} else {
				LOG.info(Log.op("deleteObject").msg("deleteObject with new sdk oldbucket").kv("isOpen", isOpen)
						.kv("key", key).toString());
				flag = iHomeNosOldClient.doesObjectExist(bucketName, key);
				if (flag) {
					LOG.info(Log.op("deleteObject").msg("deleteObject with new sdk delete really in oldbucket")
							.kv("isOpen", isOpen).kv("key", key).toString());
					iHomeNosOldClient.deleteObject(bucketName, key);
					return;
				}
			}
		} else {
			LOG.info(Log.op("deleteObject").msg("deleteObject without oldbucket").kv("isOpen", isOpen).kv("key", key)
					.toString());
			super.deleteObject(bucketName, key);
		}
	}

	/**
	 * 获取对象元数据
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @param destinationFile
	 *            目标文件
	 * @return 对象元数据
	 * @throws ClientException
	 *             客户端异常
	 * @throws ServiceException
	 *             服务端异常
	 */
	public ObjectMetadata getObject(String bucketName, String key, File destinationFile)
			throws ClientException, ServiceException {
		GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, key);
		return getObject(getObjectRequest, destinationFile);
	}

	/**
	 * 获取对象元数据
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @return 对象元数据
	 * @throws ClientException
	 *             客户端异常
	 * @throws ServiceException
	 *             服务端异常
	 */
	public NOSObject getObject(String bucketName, String key) {
		return getObject(new GetObjectRequest(bucketName, key));
	}

	/**
	 * 获取对象，并以字符串的形式返回(UTF-8)
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @return 目标文件的内容
	 * @throws ClientException
	 *             客户端异常
	 * @throws ServiceException
	 *             服务端异常
	 */
	public List<String> getObjectAsStringLine(String bucketName, String key) throws ClientException, ServiceException {
		return getObjectAsStringLine(bucketName, key, BaseConst.DEFAULT_CHARSET);
	}

	/**
	 * 获取对象，并以字符串的形式返回
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @return 目标文件的内容
	 * @throws ClientException
	 *             客户端异常
	 * @throws ServiceException
	 *             服务端异常
	 */
	public List<String> getObjectAsStringLine(String bucketName, String key, Charset baseCharset)
			throws ClientException, ServiceException {
		NOSObject nosObject = getObject(bucketName, key);
		NOSObjectInputStream nbis = nosObject.getObjectContent();
		List<String> content = null;
		try {
			content = IOUtils.readLines(nbis, baseCharset);
		} catch (IOException e) {
			throw new ClientException("toString fail", e);
		} finally {
			IOUtils.closeQuietly(nbis);
		}
		return content;
	}

	/**
	 * 获取对象，并以字符串的形式返回
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @return 目标文件的内容
	 * @throws ClientException
	 *             客户端异常
	 * @throws ServiceException
	 *             服务端异常
	 */
	public String getObjectAsString(String bucketName, String key, Charset baseCharset)
			throws ClientException, ServiceException {
		NOSObject nosObject = getObject(bucketName, key);
		NOSObjectInputStream nbis = nosObject.getObjectContent();
		String content = null;
		try {
			content = IOUtils.toString(nbis, baseCharset);
		} catch (IOException e) {
			throw new ClientException("toString fail", e);
		} finally {
			IOUtils.closeQuietly(nbis);
		}
		return content;
	}

	/**
	 * 获取对象，并以字符串的形式返回(UTF-8)
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @return 目标文件的内容
	 * @throws ClientException
	 *             客户端异常
	 * @throws ServiceException
	 *             服务端异常
	 */
	public String getObjectAsString(String bucketName, String key) throws ClientException, ServiceException {
		return getObjectAsString(bucketName, key, BaseConst.DEFAULT_CHARSET);
	}

	/**
	 * 分块上传本地文件
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @param f
	 *            文件
	 * @return
	 */
	public UploadResult putObjectWithBlock(String bucketName, String key, File f) {
		Upload upload = transferManager.upload(bucketName, key, f);
		UploadResult result = null;
		try {
			result = upload.waitForUploadResult();
		} catch (ClientException | InterruptedException e) {
			throw new ClientException("putObjectWithBlock fail", e);
		}
		return result;
	}

	/**
	 * 压缩上传文件
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @param f
	 *            文件
	 * @param zipFileName
	 *            压缩文件内部名称
	 * @param path
	 *            压缩文件存放路径(包含文件名)
	 * @return
	 */
	public UploadResult putObjectWithZip(String bucketName, String key, File f, String zipFileName, String path) {
		try {
			zipFile(f, zipFileName, path);
		} catch (Exception ex) {
			throw new ClientException("putObjectWithZip zip file fail", ex);
		}
		File zipFile = new File(path);
		Upload upload = transferManager.upload(bucketName, key, zipFile);
		UploadResult result = null;
		try {
			result = upload.waitForUploadResult();
		} catch (ClientException | InterruptedException e) {
			throw new ClientException("putObjectWithZip fail", e);
		}
		return result;
	}

	/**
	 * 流式分块上传
	 * 
	 * @param bucketName
	 *            桶名
	 * @param key
	 *            对象名
	 * @param len
	 *            文件长度
	 * @param inputStream
	 *            文件流
	 * @return
	 */
	public UploadResult putObjectWithBlockStream(String bucketName, String key, long len, InputStream inputStream) {
		ObjectMetadata objectMetadata = new ObjectMetadata();
		objectMetadata.setContentLength(len);
		Upload upload = transferManager.upload(bucketName, key, inputStream, objectMetadata);
		UploadResult result = null;
		try {
			result = upload.waitForUploadResult();
		} catch (ClientException | InterruptedException e) {
			throw new ClientException("putObjectWithBlockStream fail", e);
		}
		return result;
	}

	/**
	 * 多线程分块文件下载
	 * 
	 * @param bucket
	 *            桶名
	 * @param key
	 *            对象名
	 * @param path
	 *            文件路径
	 */
	public void downloadFileWithMultiThread(String bucket, String key, String path, String destfileName) {
		ObjectMetadata om = this.getObjectMetadata(bucket, key);
		long length = om.getContentLength();
		int fileCnts = (int) (length / DOWNLOAD_BLOCK_SIZE + 1);
		CountDownLatch cdl = new CountDownLatch(fileCnts);
		List<String> fileList = new ArrayList<>();
		LOG.info(Log.op("downloadFileWithMultiThread")
				.msg("start to download, totalBytes=" + length + ", fileCounts=" + fileCnts).toString());
		long lastEndBytes = 0;
		ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
		for (int i = 0; i < fileCnts; i++) {
			// 多线程下载
			long startBytes = lastEndBytes;
			long endBytes = startBytes + DOWNLOAD_BLOCK_SIZE;
			lastEndBytes = endBytes + 1;
			// 处理末尾
			if (endBytes >= length) {
				endBytes = length;
			}
			String destFileName = key + "-" + i;
			if (StringUtils.isNotBlank(path)) {
				destFileName = path + File.separator + destFileName;
			}
			NosDownloader downloader = new NosDownloader(this, bucket, key, startBytes, endBytes, cdl, destFileName);
			threadPool.execute(downloader);
			fileList.add(destFileName);
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				LOG.error((Log.op("downloadFileWithMultiThread").msg("sleep with error").toString()));
				throw new ClientException("downloadFileWithMultiThread sleep fail", e);
			}
			LOG.info(Log.op("downloadFileWithMultiThread").msg("add task:" + destFileName).toString());
		}
		try {
			cdl.await();
		} catch (InterruptedException e) {
			LOG.error((Log.op("downloadFileWithMultiThread").msg("await with error").toString()));
			throw new ClientException("downloadFileWithMultiThread await fail", e);
		}
		// 组合文件
		LOG.info(Log.op("downloadFileWithMultiThread").msg("start to merge file").toString());
		for (String fileName : fileList) {
			InputStream is = null;
			OutputStream os = null;
			try {
				String finalFilePath = destfileName;
				if (StringUtils.isNotBlank(path)) {
					finalFilePath = path + File.separator + destfileName;
				}
				is = new FileInputStream(new File(fileName));
				os = new FileOutputStream(new File(finalFilePath), true);
				IOUtils.copy(is, os);
			} catch (IOException e) {
				LOG.error((Log.op("downloadFileWithMultiThread").msg("merge file with error").toString()));
				throw new ClientException("downloadFileWithMultiThread merge file fail", e);
			} finally {
				IOUtils.closeQuietly(is);
				IOUtils.closeQuietly(os);
				FileUtils.deleteQuietly(new File(fileName));
			}
		}
		LOG.info(Log.op("downloadFileWithMultiThread").msg("download finish").toString());
		threadPool.shutdown();

	}

	// 压缩文件
	private void zipFile(File f, String filename, String path) throws IOException {
		FileInputStream fis = new FileInputStream(f);
		BufferedInputStream bis = new BufferedInputStream(fis);
		ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(path));
		BufferedOutputStream bos = new BufferedOutputStream(zos);
		zos.putNextEntry(new ZipEntry(filename));
		byte[] b = new byte[ZIP_BUFFER_SIZE];
		while (true) {
			int len = bis.read(b);
			if (len == -1) {
				break;
			}
			bos.write(b, 0, len);
		}
		fis.close();
		zos.close();
	}

}
